import { Layer } from '../Layer';
import { IconDefault } from './Icon.Default';
import * as Util from '../../core/Util';
import { toLatLng as latLng } from '../../geo/LatLng';
import { toPoint as point } from '../../geometry/Point';
import * as DomUtil from '../../dom/DomUtil';
import * as DomEvent from '../../dom/DomEvent';
import { MarkerDrag } from './Marker.Drag';

/*
 * @class Marker
 * @inherits Interactive layer
 * @aka L.Marker
 * L.Marker is used to display clickable/draggable icons on the map. Extends `Layer`.
 *
 * @example
 *
 * ```js
 * L.marker([50.5, 30.5]).addTo(map);
 * ```
 */

export var Marker = Layer.extend({
  // @section
  // @aka Marker options
  options: {
    // @option icon: Icon = *
    // Icon instance to use for rendering the marker.
    // See [Icon documentation](#L.Icon) for details on how to customize the marker icon.
    // If not specified, a common instance of `L.Icon.Default` is used.
    icon: new IconDefault(),

    // Option inherited from "Interactive layer" abstract class
    interactive: true,

    // @option keyboard: Boolean = true
    // Whether the marker can be tabbed to with a keyboard and clicked by pressing enter.
    keyboard: true,

    // @option title: String = ''
    // Text for the browser tooltip that appear on marker hover (no tooltip by default).
    // [Useful for accessibility](https://leafletjs.com/examples/accessibility/#markers-must-be-labelled).
    title: '',

    // @option alt: String = 'Marker'
    // Text for the `alt` attribute of the icon image.
    // [Useful for accessibility](https://leafletjs.com/examples/accessibility/#markers-must-be-labelled).
    alt: 'Marker',

    // @option zIndexOffset: Number = 0
    // By default, marker images zIndex is set automatically based on its latitude. Use this option if you want to put the marker on top of all others (or below), specifying a high value like `1000` (or high negative value, respectively).
    zIndexOffset: 0,

    // @option opacity: Number = 1.0
    // The opacity of the marker.
    opacity: 1,

    // @option riseOnHover: Boolean = false
    // If `true`, the marker will get on top of others when you hover the mouse over it.
    riseOnHover: false,

    // @option riseOffset: Number = 250
    // The z-index offset used for the `riseOnHover` feature.
    riseOffset: 250,

    // @option pane: String = 'markerPane'
    // `Map pane` where the markers icon will be added.
    pane: 'markerPane',

    // @option shadowPane: String = 'shadowPane'
    // `Map pane` where the markers shadow will be added.
    shadowPane: 'shadowPane',

    // @option bubblingMouseEvents: Boolean = false
    // When `true`, a mouse event on this marker will trigger the same event on the map
    // (unless [`L.DomEvent.stopPropagation`](#domevent-stoppropagation) is used).
    bubblingMouseEvents: false,

    // @option autoPanOnFocus: Boolean = true
    // When `true`, the map will pan whenever the marker is focused (via
    // e.g. pressing `tab` on the keyboard) to ensure the marker is
    // visible within the map's bounds
    autoPanOnFocus: true,

    // @section Draggable marker options
    // @option draggable: Boolean = false
    // Whether the marker is draggable with mouse/touch or not.
    draggable: false,

    // @option autoPan: Boolean = false
    // Whether to pan the map when dragging this marker near its edge or not.
    autoPan: false,

    // @option autoPanPadding: Point = Point(50, 50)
    // Distance (in pixels to the left/right and to the top/bottom) of the
    // map edge to start panning the map.
    autoPanPadding: [50, 50],

    // @option autoPanSpeed: Number = 10
    // Number of pixels the map should pan by.
    autoPanSpeed: 10,
  },

  /* @section
   *
   * In addition to [shared layer methods](#Layer) like `addTo()` and `remove()` and [popup methods](#Popup) like bindPopup() you can also use the following methods:
   */

  initialize: function (latlng, options) {
    Util.setOptions(this, options);
    this._latlng = latLng(latlng);
  },

  onAdd: function (map) {
    this._zoomAnimated = this._zoomAnimated && map.options.markerZoomAnimation;

    if (this._zoomAnimated) {
      map.on('zoomanim', this._animateZoom, this);
    }

    this._initIcon();
    this.update();
  },

  onRemove: function (map) {
    if (this.dragging && this.dragging.enabled()) {
      this.options.draggable = true;
      this.dragging.removeHooks();
    }
    delete this.dragging;

    if (this._zoomAnimated) {
      map.off('zoomanim', this._animateZoom, this);
    }

    this._removeIcon();
    this._removeShadow();
  },

  getEvents: function () {
    return {
      zoom: this.update,
      viewreset: this.update,
    };
  },

  // @method getLatLng: LatLng
  // Returns the current geographical position of the marker.
  getLatLng: function () {
    return this._latlng;
  },

  // @method setLatLng(latlng: LatLng): this
  // Changes the marker position to the given point.
  setLatLng: function (latlng) {
    var oldLatLng = this._latlng;
    this._latlng = latLng(latlng);
    this.update();

    // @event move: Event
    // Fired when the marker is moved via [`setLatLng`](#marker-setlatlng) or by [dragging](#marker-dragging). Old and new coordinates are included in event arguments as `oldLatLng`, `latlng`.
    return this.fire('move', { oldLatLng: oldLatLng, latlng: this._latlng });
  },

  // @method setZIndexOffset(offset: Number): this
  // Changes the [zIndex offset](#marker-zindexoffset) of the marker.
  setZIndexOffset: function (offset) {
    this.options.zIndexOffset = offset;
    return this.update();
  },

  // @method getIcon: Icon
  // Returns the current icon used by the marker
  getIcon: function () {
    return this.options.icon;
  },

  // @method setIcon(icon: Icon): this
  // Changes the marker icon.
  setIcon: function (icon) {
    this.options.icon = icon;

    if (this._map) {
      this._initIcon();
      this.update();
    }

    if (this._popup) {
      this.bindPopup(this._popup, this._popup.options);
    }

    return this;
  },

  getElement: function () {
    return this._icon;
  },

  update: function () {
    if (this._icon && this._map) {
      var pos = this._map.latLngToLayerPoint(this._latlng).round();
      this._setPos(pos);
    }

    return this;
  },

  _initIcon: function () {
    var options = this.options,
      classToAdd = 'leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide');

    var icon = options.icon.createIcon(this._icon),
      addIcon = false;

    // if we're not reusing the icon, remove the old one and init new one
    if (icon !== this._icon) {
      if (this._icon) {
        this._removeIcon();
      }
      addIcon = true;

      if (options.title) {
        icon.title = options.title;
      }

      if (icon.tagName === 'IMG') {
        icon.alt = options.alt || '';
      }
    }

    DomUtil.addClass(icon, classToAdd);

    if (options.keyboard) {
      icon.tabIndex = '0';
      icon.setAttribute('role', 'button');
    }

    this._icon = icon;

    if (options.riseOnHover) {
      this.on({
        mouseover: this._bringToFront,
        mouseout: this._resetZIndex,
      });
    }

    if (this.options.autoPanOnFocus) {
      DomEvent.on(icon, 'focus', this._panOnFocus, this);
    }

    var newShadow = options.icon.createShadow(this._shadow),
      addShadow = false;

    if (newShadow !== this._shadow) {
      this._removeShadow();
      addShadow = true;
    }

    if (newShadow) {
      DomUtil.addClass(newShadow, classToAdd);
      newShadow.alt = '';
    }
    this._shadow = newShadow;

    if (options.opacity < 1) {
      this._updateOpacity();
    }

    if (addIcon) {
      this.getPane().appendChild(this._icon);
    }
    this._initInteraction();
    if (newShadow && addShadow) {
      this.getPane(options.shadowPane).appendChild(this._shadow);
    }
  },

  _removeIcon: function () {
    if (this.options.riseOnHover) {
      this.off({
        mouseover: this._bringToFront,
        mouseout: this._resetZIndex,
      });
    }

    if (this.options.autoPanOnFocus) {
      DomEvent.off(this._icon, 'focus', this._panOnFocus, this);
    }

    DomUtil.remove(this._icon);
    this.removeInteractiveTarget(this._icon);

    this._icon = null;
  },

  _removeShadow: function () {
    if (this._shadow) {
      DomUtil.remove(this._shadow);
    }
    this._shadow = null;
  },

  _setPos: function (pos) {
    if (this._icon) {
      DomUtil.setPosition(this._icon, pos);
    }

    if (this._shadow) {
      DomUtil.setPosition(this._shadow, pos);
    }

    this._zIndex = pos.y + this.options.zIndexOffset;

    this._resetZIndex();
  },

  _updateZIndex: function (offset) {
    if (this._icon) {
      this._icon.style.zIndex = this._zIndex + offset;
    }
  },

  _animateZoom: function (opt) {
    var pos = this._map
      ._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center)
      .round();

    this._setPos(pos);
  },

  _initInteraction: function () {
    if (!this.options.interactive) {
      return;
    }

    DomUtil.addClass(this._icon, 'leaflet-interactive');

    this.addInteractiveTarget(this._icon);

    if (MarkerDrag) {
      var draggable = this.options.draggable;
      if (this.dragging) {
        draggable = this.dragging.enabled();
        this.dragging.disable();
      }

      this.dragging = new MarkerDrag(this);

      if (draggable) {
        this.dragging.enable();
      }
    }
  },

  // @method setOpacity(opacity: Number): this
  // Changes the opacity of the marker.
  setOpacity: function (opacity) {
    this.options.opacity = opacity;
    if (this._map) {
      this._updateOpacity();
    }

    return this;
  },

  _updateOpacity: function () {
    var opacity = this.options.opacity;

    if (this._icon) {
      DomUtil.setOpacity(this._icon, opacity);
    }

    if (this._shadow) {
      DomUtil.setOpacity(this._shadow, opacity);
    }
  },

  _bringToFront: function () {
    this._updateZIndex(this.options.riseOffset);
  },

  _resetZIndex: function () {
    this._updateZIndex(0);
  },

  _panOnFocus: function () {
    var map = this._map;
    if (!map) {
      return;
    }

    var iconOpts = this.options.icon.options;
    var size = iconOpts.iconSize ? point(iconOpts.iconSize) : point(0, 0);
    var anchor = iconOpts.iconAnchor ? point(iconOpts.iconAnchor) : point(0, 0);

    map.panInside(this._latlng, {
      paddingTopLeft: anchor,
      paddingBottomRight: size.subtract(anchor),
    });
  },

  _getPopupAnchor: function () {
    return this.options.icon.options.popupAnchor;
  },

  _getTooltipAnchor: function () {
    return this.options.icon.options.tooltipAnchor;
  },
});

// factory L.marker(latlng: LatLng, options? : Marker options)

// @factory L.marker(latlng: LatLng, options? : Marker options)
// Instantiates a Marker object given a geographical point and optionally an options object.
export function marker(latlng, options) {
  return new Marker(latlng, options);
}
